let path = require('path');
let fs = require('fs');
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let types = require('@babel/types');
let ejs = require('ejs');
let {SyncHook} = require('../tapable')

class Compiler {

    constructor(config) {
        this.config = config;
        // 执行位置的根目录
        this.root = process.cwd();
        // 入口文件
        this.entryId;
        // 所有要依赖的模块
        this.modules = {};
        this.hooks = {
            entryOption: new SyncHook(['compiler']),
            run: new SyncHook(['compiler']),
            beforeCompile: new SyncHook(['compiler']),
            afterCompile: new SyncHook(['compiler']),
            emit: new SyncHook(['compiler']),
            afterEmit: new SyncHook(['compiler']),
            done: new SyncHook(['compiler']),
        }
        if (Array.isArray(this.config.plugins)) {
            // 运行所有的plugin
            this.config.plugins.forEach(p => p.apply(this))
        }
    }

    // 读取文件内容
    getResource(fileName) {
        let content = fs.readFileSync(fileName, 'utf8')
        // 获取匹配规则
        let rules = this.config.module.rules;
        // 读取内容的时候 如果我们文件名匹配到了 需要用loader来处理我们的内容
        for (let rule of rules) {
            let {test, use} = rule;
            // 匹配后缀名
            if (test.test(fileName)) {
                let len = use.length - 1
                function normalLoader() {
                    // 引入loader
                    let loader = require(use[len--])
                    // 把内容传入到loader函数中，将结果返回作为新的内容
                    content = loader(content)
                    if (len >= 0) normalLoader() 
                }
                normalLoader()
            }
            
        }
        return content
    }

    // 编译模块
    parse(content, parentPath) {
        // 1) 把代码变成 ast语法树 esprima  => babylon
        let ast = babylon.parse(content);
        let dependencies = [];
        // 2) 遍历树找到对应的节点 estraverse => @babel/traverse  
        traverse(ast, {
            CallExpression(p) {
                let node = p.node
                if (node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__'
                    let fileName = node.arguments[0].value
                    // 如果没有扩展名就给他加一个
                    fileName = fileName + (path.extname(fileName) ? '' : '.js')
                    // 添加父级路径
                    fileName = './' + path.join(parentPath, fileName)
                    // 将其引用的模块存起来
                    dependencies.push(fileName)
                    // 替换节点
                    node.arguments[0] = types.stringLiteral(fileName)
                }
            }
        })
        // 3) 重新生成树 escodegen => @babel/generator
        let reCode = generator(ast)

        return {sourceCode: reCode.code, dependencies}
    }

    // 创建模块
    buildMoudle(moduleName, isEntry) {
        let content = this.getResource(moduleName);
        // 获取相对路径
        let relativePath = './' + path.relative(this.root, moduleName);
        // 将路口文件存起来
        if (isEntry) {
            this.entryId = relativePath
        }
        // path.dirname 获取当前模块的父级路径
        let { sourceCode, dependencies } = this.parse(content, path.dirname(relativePath))
        this.modules[relativePath] = sourceCode
        dependencies.forEach(dep => this.buildMoudle(path.join(this.root, dep), false))
    }

    emitFile() {
        // 获取模板
        let templateStr = this.getResource(path.resolve(__dirname, './template.js'))
        // 渲染代码
        let reCode = ejs.render(templateStr, {
            entryId: this.entryId,
            modules: this.modules
        })
        // 获取输出地址
        
        let outputPath = path.join(this.config.output.path, this.config.output.filename)
        // 生成文件
        fs.writeFileSync(outputPath, reCode);
    }

    run() {
        this.hooks.run.call(this);
        this.hooks.beforeCompile.call(this);
        this.buildMoudle(path.join(this.root, this.config.entry), true)
        this.hooks.afterCompile.call(this);
        this.hooks.emit.call(this);
        this.emitFile();
        this.hooks.afterEmit.call(this);
        this.hooks.done.call(this);
    }
}

module.exports = Compiler;